package org.arclib.sound;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;

import org.apache.log4j.Logger;

/**
 * A piece of music loaded and playable within the game. Only one piece of music
 * can play at any given time and a channel is reserved so music will always
 * play.
 * 
 * @author kevin
 * @author Nathan Sweet <misc@n4te.com>
 */
public class Music
{
	/** The music currently being played or null if none */
	private static Music currentMusic;

	/**
	 * Poll the state of the current music. This causes streaming music to
	 * stream and checks listeners. Note that if you're using a game container
	 * this will be auto-magically called for you.
	 * 
	 * @param delta
	 *            The amount of time since last poll
	 */
	public static void poll(int delta)
	{
		if (currentMusic != null)
		{
			SoundStore.get().poll(delta);
			if (!SoundStore.get().isMusicPlaying())
			{
				Music oldMusic = currentMusic;
				currentMusic = null;
				oldMusic.fireMusicEnded();
			} else
			{
				currentMusic.update(delta);
			}
		}
	}

	/** The sound from FECK representing this music */
	private Audio sound;
	/** True if the music is playing */
	private boolean playing;
	/** The list of listeners waiting for notification that the music ended */
	private ArrayList listeners = new ArrayList();
	/** The volume of this music */
	private float volume = 1.0f;
	/** Start gain for fading in/out */
	private float fadeStartGain;
	/** End gain for fading in/out */
	private float fadeEndGain;
	/** Countdown for fading in/out */
	private int fadeTime;
	/** Duration for fading in/out */
	private int fadeDuration;
	/** True if music should be stopped after fading in/out */
	private boolean stopAfterFade;

	/**
	 * Create and load a piece of music (either OGG or MOD/XM)
	 * 
	 * @param ref
	 *            The location of the music
	 * @throws SlickException
	 */
	public Music(String ref) throws IOException
	{
		this(ref, false);
	}

	/**
	 * Create and load a piece of music (either OGG or MOD/XM)
	 * 
	 * @param ref
	 *            The location of the music
	 * @throws SlickException
	 */
	public Music(URL ref) throws IOException
	{
		this(ref, false);
	}

	/**
	 * Create and load a piece of music (either OGG or MOD/XM)
	 * 
	 * @param url
	 *            The location of the music
	 * @param streamingHint
	 *            A hint to indicate whether streaming should be used if
	 *            possible
	 * @throws SlickException
	 */
	public Music(URL url, boolean streamingHint) throws IOException
	{
		SoundStore.get().init();
		String ref = url.getFile();

		try
		{
			if (ref.toLowerCase().endsWith(".ogg"))
			{
				if (streamingHint)
				{
					sound = SoundStore.get().getOggStream(url);
				} else
				{
					sound = SoundStore.get().getOgg(url.openStream());
				}
			} else if (ref.toLowerCase().endsWith(".wav"))
			{
				sound = SoundStore.get().getWAV(url.openStream());
			} else
			{
				throw new IOException(
						"Only .xm, .mod, .ogg, and .aif/f are currently supported.");
			}
		} catch (Exception e)
		{
			log.error(e);
			throw new IOException("Failed to load sound: " + url);
		}
	}

	/**
	 * Create and load a piece of music (either OGG or MOD/XM)
	 * 
	 * @param ref
	 *            The location of the music
	 * @param streamingHint
	 *            A hint to indicate whether streaming should be used if
	 *            possible
	 * @throws SlickException
	 */
	public Music(String ref, boolean streamingHint) throws IOException
	{
		SoundStore.get().init();

		try
		{
			if (ref.toLowerCase().endsWith(".ogg"))
			{
				if (streamingHint)
				{
					sound = SoundStore.get().getOggStream(ref);
				} else
				{
					sound = SoundStore.get().getOgg(ref);
				}
			} else if (ref.toLowerCase().endsWith(".wav"))
			{
				sound = SoundStore.get().getWAV(ref);
			} else
			{
				throw new IOException(
						"Only .xm, .mod, .ogg, and .aif/f are currently supported.");
			}
		} catch (Exception e)
		{
			log.error(e);
			throw new IOException("Failed to load sound: " + ref);
		}
	}

	/**
	 * Add a listener to this music
	 * 
	 * @param listener
	 *            The listener to add
	 */
	public void addListener(MusicListener listener)
	{
		listeners.add(listener);
	}

	/**
	 * Remove a listener from this music
	 * 
	 * @param listener
	 *            The listener to remove
	 */
	public void removeListener(MusicListener listener)
	{
		listeners.remove(listener);
	}

	/**
	 * Fire notifications that this music ended
	 */
	private void fireMusicEnded()
	{
		playing = false;
		for (int i = 0; i < listeners.size(); i++)
		{
			((MusicListener) listeners.get(i)).musicEnded(this);
		}
	}

	/**
	 * Fire notifications that this music was swapped out
	 * 
	 * @param newMusic
	 *            The new music that will be played
	 */
	private void fireMusicSwapped(Music newMusic)
	{
		playing = false;
		for (int i = 0; i < listeners.size(); i++)
		{
			((MusicListener) listeners.get(i)).musicSwapped(this, newMusic);
		}
	}

	/**
	 * Loop the music
	 */
	public void loop()
	{
		loop(1.0f, volume * SoundStore.get().getMusicVolume());
	}

	/**
	 * Play the music
	 */
	public void play()
	{
		play(1.0f, volume * SoundStore.get().getMusicVolume());
	}

	/**
	 * Play the music at a given pitch and volume
	 * 
	 * @param pitch
	 *            The pitch to play the music at (1.0 = default)
	 * @param volume
	 *            The volume to play the music at (1.0 = default)
	 */
	public void play(float pitch, float volume)
	{
		startMusic(pitch, volume, false);
	}

	/**
	 * Loop the music at a given pitch and volume
	 * 
	 * @param pitch
	 *            The pitch to play the music at (1.0 = default)
	 * @param volume
	 *            The volume to play the music at (1.0 = default)
	 */
	public void loop(float pitch, float volume)
	{
		startMusic(pitch, volume, true);
	}

	/**
	 * play or loop the music at a given pitch and volume
	 * 
	 * @param pitch
	 *            The pitch to play the music at (1.0 = default)
	 * @param volume
	 *            The volume to play the music at (1.0 = default)
	 * @param loop
	 *            if false the music is played once, the music is looped
	 *            otherwise
	 */
	private void startMusic(float pitch, float volume, boolean loop)
	{
		if (currentMusic != null)
		{
			currentMusic.stop();
			currentMusic.fireMusicSwapped(this);
		}

		currentMusic = this;
		if (volume < 0.0f)
			volume = 0.0f;
		if (volume > 1.0f)
			volume = 1.0f;

		setVolume(volume);
		sound.playAsMusic(pitch, volume, loop);
		playing = true;
	}

	/**
	 * Pause the music playback
	 */
	public void pause()
	{
		playing = false;
		Audio.pauseMusic();
	}

	/**
	 * Stop the music playing
	 */
	public void stop()
	{
		sound.stop();
	}

	/**
	 * Resume the music playback
	 */
	public void resume()
	{
		playing = true;
		Audio.restartMusic();
	}

	/**
	 * Check if the music is being played
	 * 
	 * @return True if the music is being played
	 */
	public boolean playing()
	{
		return (currentMusic == this) && (playing);
	}

	/**
	 * Set the volume of the music as a factor of the global volume setting
	 * 
	 * @param volume
	 *            The volume to play music at. 0 - 1, 1 is Max
	 */
	public void setVolume(float volume)
	{
		// Bounds check
		if (volume > 1)
		{
			volume = 1;
		} else if (volume < 0)
		{
			volume = 0;
		}

		this.volume = volume;
		// This sound is being played as music
		if (currentMusic == this)
		{
			SoundStore.get().setCurrentMusicVolume(volume);
		}
	}

	/**
	 * Get the individual volume of the music
	 * 
	 * @return The volume of this music, still effected by global SoundStore
	 *         volume. 0 - 1, 1 is Max
	 */
	public float getVolume()
	{
		return volume;
	}

	/**
	 * Fade this music to the volume specified
	 * 
	 * @param duration
	 *            Fade time in milliseconds.
	 * @param endVolume
	 *            The target volume
	 * @param stopAfterFade
	 *            True if music should be stopped after fading in/out
	 */
	public void fade(int duration, float endVolume, boolean stopAfterFade)
	{
		this.stopAfterFade = stopAfterFade;
		fadeStartGain = volume;
		fadeEndGain = endVolume;
		fadeDuration = duration;
		fadeTime = duration;
	}

	/**
	 * Update the current music applying any effects that need to updated per
	 * tick.
	 * 
	 * @param delta
	 *            The amount of time in milliseconds thats passed since last
	 *            update
	 */
	void update(int delta)
	{
		if (!playing)
		{
			return;
		}

		if (fadeTime > 0)
		{
			fadeTime -= delta;
			if (fadeTime < 0)
			{
				fadeTime = 0;
				if (stopAfterFade)
				{
					stop();
					return;
				}
			}

			float offset = (fadeEndGain - fadeStartGain)
					* (1 - (fadeTime / (float) fadeDuration));
			setVolume(fadeStartGain + offset);
		}
	}

	/**
	 * Seeks to a position in the music. For streaming music, seeking before the
	 * current position causes the stream to be reloaded.
	 * 
	 * @param position
	 *            Position in seconds.
	 * @return True if the seek was successful
	 */
	public boolean setPosition(float position)
	{
		return sound.setPosition(position);
	}

	/**
	 * The position into the sound thats being played
	 * 
	 * @return The current position in seconds.
	 */
	public float getPosition()
	{
		return sound.getPosition();
	}

	private static Logger log = Logger.getLogger(Music.class);
}
